/*
 * Copyright 2017 - 2025 the original author or authors.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see [https://www.gnu.org/licenses/]
 */

package infra.persistence;

import org.jspecify.annotations.Nullable;

import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Objects;

import infra.beans.BeanMetadata;
import infra.beans.BeanProperty;
import infra.core.annotation.MergedAnnotation;
import infra.core.annotation.MergedAnnotations;
import infra.core.style.ToStringBuilder;
import infra.util.StringUtils;

/**
 * Metadata class representing the structure and configuration of an entity in a database context.
 *
 * <p>This class encapsulates metadata about an entity, including its table name, ID property,
 * column mappings, and other related properties. It is used to provide a centralized source
 * of information for operations involving the entity, such as database queries or persistence.
 *
 * <p>The metadata includes details about the entity's properties, their corresponding database
 * columns, and annotations applied to the entity class. It also provides utility methods for
 * retrieving specific properties, annotations, and determining whether certain annotations are present.
 *
 * <p>Instances of this class are immutable once created, ensuring thread safety and consistency
 * in usage across different parts of an application.
 *
 * <p>Key features include:
 * - Mapping between entity properties and database columns.
 * - Support for identifying auto-generated IDs.
 * - Retrieval of merged annotations for the entity class.
 * - Efficient lookup of properties by name.
 *
 * <p>This class is typically instantiated by a metadata factory or builder and is not intended
 * for direct instantiation by application code.
 *
 * @author <a href="https://github.com/TAKETODAY">Harry Yang</a>
 * @since 4.0 2022/8/16 22:43
 */
public class EntityMetadata {
  public final String tableName;

  public final BeanMetadata root;

  public final Class<?> entityClass;

  @Nullable
  public final String idColumnName;

  @Nullable
  public final EntityProperty idProperty;

  @Nullable
  public final EntityProperty refIdProperty;

  public final BeanProperty[] beanProperties;

  public final String[] columnNames;
  public final EntityProperty[] entityProperties;

  public final String[] columnNamesExcludeId;
  public final EntityProperty[] entityPropertiesExcludeId;

  // is auto generated Id
  public final boolean autoGeneratedId;

  @Nullable
  private MergedAnnotations annotations;

  private final HashMap<String, EntityProperty> propertyMap;

  protected EntityMetadata(BeanMetadata root, Class<?> entityClass, @Nullable EntityProperty idProperty, String tableName,
          @Nullable EntityProperty refIdProperty, List<BeanProperty> beanProperties, List<String> columnNames, List<EntityProperty> entityProperties) {
    this.root = root;
    this.tableName = tableName;
    this.idProperty = idProperty;
    this.entityClass = entityClass;
    this.refIdProperty = refIdProperty;
    this.propertyMap = mapProperties(entityProperties);
    this.idColumnName = idProperty != null ? idProperty.columnName : null;
    this.columnNames = StringUtils.toStringArray(columnNames);
    this.beanProperties = beanProperties.toArray(new BeanProperty[0]);
    this.entityProperties = entityProperties.toArray(new EntityProperty[0]);

    if (idProperty != null) {
      entityProperties.remove(idProperty);
      columnNames.remove(idProperty.columnName);
    }

    this.autoGeneratedId = determineGeneratedId(idProperty);
    this.columnNamesExcludeId = StringUtils.toStringArray(columnNames);
    this.entityPropertiesExcludeId = entityProperties.toArray(new EntityProperty[0]);
  }

  private HashMap<String, EntityProperty> mapProperties(List<EntityProperty> entityProperties) {
    HashMap<String, EntityProperty> propertyMap = new HashMap<>();
    for (EntityProperty property : entityProperties) {
      propertyMap.put(property.property.getName(), property);
    }
    return propertyMap;
  }

  private boolean determineGeneratedId(@Nullable EntityProperty idProperty) {
    if (idProperty != null) {
      return idProperty.isPresent(GeneratedId.class);
    }
    return false;
  }

  /**
   * Returns the ID property of the entity.
   *
   * <p>This method retrieves the ID property by accessing the {@code idProperty} field.
   * If the {@code idProperty} field is null, an {@link IllegalEntityException} is thrown
   * to indicate that the ID property is required but not set.
   *
   * @return the ID property of the entity
   * @throws IllegalEntityException if the ID property is not set
   */
  public EntityProperty idProperty() throws IllegalEntityException {
    EntityProperty idProperty = this.idProperty;
    if (idProperty == null) {
      throw new IllegalEntityException("ID property is required");
    }
    return idProperty;
  }

  /**
   * Finds and returns the entity property associated with the given name.
   *
   * <p>This method retrieves the property by looking up the internal property map
   * using the provided name. If no property is found, the method returns {@code null}.
   *
   * @param name the name of the property to find; must not be null
   * @return the {@code EntityProperty} associated with the specified name,
   * or {@code null} if no such property exists
   */
  @Nullable
  public EntityProperty findProperty(String name) {
    return propertyMap.get(name);
  }

  /**
   * Retrieves the merged annotations associated with the entity class.
   *
   * <p>
   * This method returns the cached annotations if they have already been loaded.
   * If no annotations are cached, it retrieves them from the entity class using
   * {@link MergedAnnotations#from(AnnotatedElement)} and caches the result for future use.
   *
   * @return the merged annotations for the entity class
   */
  public MergedAnnotations getAnnotations() {
    MergedAnnotations annotations = this.annotations;
    if (annotations == null) {
      annotations = MergedAnnotations.from(entityClass);
      this.annotations = annotations;
    }
    return annotations;
  }

  /**
   * Retrieves the merged annotation of the specified type for the entity class.
   *
   * @param annType the annotation type to retrieve; must not be null
   * @return the merged annotation of the specified type, or an empty annotation if not present
   */
  public <A extends Annotation> MergedAnnotation<A> getAnnotation(Class<A> annType) {
    return getAnnotations().get(annType);
  }

  /**
   * Determines whether the specified annotation type is present on the entity class.
   *
   * <p>This method checks if the given annotation type is either directly present or meta-present
   * on the entity class by delegating to the {@link MergedAnnotations#isPresent(Class)} method.
   *
   * @param annType the annotation type to check; must not be null
   * @return true if the specified annotation type is present on the entity class, false otherwise
   */
  public <A extends Annotation> boolean isPresent(Class<A> annType) {
    return getAnnotations().isPresent(annType);
  }

  @Override
  public String toString() {
    return ToStringBuilder.forInstance(this)
            .append("tableName", tableName)
            .append("columnNames", columnNames)
            .append("entityClass", entityClass)
            .append("idProperty", idProperty)
            .append("beanProperties", beanProperties)
            .append("entityProperties", entityProperties)
            .toString();
  }

  @Override
  public boolean equals(Object o) {
    return this == o
            || (o instanceof EntityMetadata that
            && Objects.equals(tableName, that.tableName)
            && Objects.equals(idProperty, that.idProperty)
            && Arrays.equals(columnNames, that.columnNames)
            && Objects.equals(entityClass, that.entityClass)
            && Arrays.equals(beanProperties, that.beanProperties)
            && Arrays.equals(entityProperties, that.entityProperties));
  }

  @Override
  public int hashCode() {
    int result = Objects.hash(tableName, entityClass, idProperty);
    result = 31 * result + Arrays.hashCode(beanProperties);
    result = 31 * result + Arrays.hashCode(columnNames);
    result = 31 * result + Arrays.hashCode(entityProperties);
    return result;
  }

}
